package com.xuecheng.search.service.impl;

import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.xuecheng.base.constant.CoursePublishStatusConstant;
import com.xuecheng.base.model.PageParams;
import com.xuecheng.search.config.ElasticsearchProperties;
import com.xuecheng.search.constant.ESConstant;
import com.xuecheng.search.dto.SearchCourseParamDTO;
import com.xuecheng.search.dto.SearchPageResultDTO;
import com.xuecheng.search.po.CourseIndex;
import com.xuecheng.search.service.ICourseSearchService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

/**
 * @author 勾新杰
 * @version 1.0
 * @description 课程搜索service实现类
 * @date 2022/9/24 22:48
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class CourseSearchServiceImpl implements ICourseSearchService {

    /**
     * elasticsearch配置属性
     */
    private final ElasticsearchProperties elasticsearchProperties;

    /**
     * elasticsearch客户端
     */
    private final RestHighLevelClient elasticsearchClient;

    /**
     * 课程分页条件搜索
     *
     * @param pageParams           分页参数
     * @param searchCourseParamDTO 条件参数
     * @return 搜索结果
     */
    @Override
    public SearchPageResultDTO<CourseIndex> pageQueryCourseIndex(PageParams pageParams, SearchCourseParamDTO searchCourseParamDTO) {
        // 1. 先准备查询请求和布尔查询
        SearchRequest searchRequest = new SearchRequest(elasticsearchProperties.getCourse().getIndexName());
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
        // 2. 当keywords不为空时，多词条匹配keywords查询，设置高亮字段
        if (StrUtil.isNotBlank(searchCourseParamDTO.getKeywords()))
            multiMatchAndHighlight(searchCourseParamDTO, boolQueryBuilder, searchRequest);
        // 3. 精确匹配，这些是一定要满足，但不参与算分
        termFilter(searchCourseParamDTO, boolQueryBuilder);
        // 4. 配置排序
        setSort(searchCourseParamDTO, searchRequest);
        // 5. 构建查询请求对象
        searchRequest.source()
                // 5.1 配置分页
                .from((int) ((pageParams.getPageNo() - 1) * pageParams.getPageSize()))
                .size(Math.toIntExact(pageParams.getPageSize()))
                // 5.2 配置布尔复杂查询
                .query(boolQueryBuilder)
                // 5.3 配置聚合
                .aggregation(AggregationBuilders.terms(ESConstant.MT_NAME_AGG)
                        .field(ESConstant.MT_NAME_FIELD)
                        .size(ESConstant.AGG_SIZE))
                .aggregation(AggregationBuilders.terms(ESConstant.ST_NAME_AGG)
                        .field(ESConstant.ST_NAME_FIELD)
                        .size(ESConstant.AGG_SIZE))
                // 5.4 配置查询出来后的结果的source只取哪些字段
                .fetchSource(elasticsearchProperties.getCourse().getSourceFields().split(","), new String[]{});
        try {
            // 6. 执行查询
            SearchResponse searchResponse = elasticsearchClient.search(searchRequest, RequestOptions.DEFAULT);
            log.debug("查询结果：{}", searchResponse.toString());
            // 7. 解析结果，封装成list
            List<CourseIndex> courseIndexList = getCourseIndexList(searchCourseParamDTO, searchResponse);
            // 8. 解析聚合结果，封装成list
            Terms mtAgg = searchResponse.getAggregations().get(ESConstant.MT_NAME_AGG);
            Terms stAgg = searchResponse.getAggregations().get(ESConstant.ST_NAME_AGG);
            List<String> mtNameList = getAggList(mtAgg);
            List<String> stNameList = getAggList(stAgg);
            // 9. 封装成返回结果
            return new SearchPageResultDTO<>(mtNameList, stNameList, courseIndexList, searchResponse.getHits().getTotalHits().value, pageParams.getPageNo(), pageParams.getPageSize());
        } catch (IOException e) {
            // 10. 捕获异常，返回空结果
            log.error("课程搜索异常：{}", e.getMessage());
            return new SearchPageResultDTO<>(Collections.emptyList(), 0, 0, 0);
        }
    }

    /**
     * 解析请求响应结果，封装成list
     *
     * @param searchCourseParamDTO 条件参数
     * @param searchResponse       搜索请求响应结果
     * @return 集合
     */
    private List<CourseIndex> getCourseIndexList(SearchCourseParamDTO searchCourseParamDTO, SearchResponse searchResponse) {
        List<CourseIndex> courseIndexList = new ArrayList<>();
        SearchHit[] hits = searchResponse.getHits().getHits();
        // 遍历响应的hit命中数据
        for (SearchHit hit : hits) {
            // 1. 获取当前hit命中项的source部分数据，并反序列化
            String source = hit.getSourceAsString();
            CourseIndex courseIndex = JSONUtil.toBean(source, CourseIndex.class);
            // 2. 如果请求参数的搜索关键字不为空，则要把对应字段替换为高亮后的结果
            if (StrUtil.isNotBlank(searchCourseParamDTO.getKeywords())) {
                // 2.1 获取高亮的有哪些字段（设置的时候只给name添加了高亮）
                Map<String, HighlightField> fields = hit.getHighlightFields();
                // 2.2 如果有，则组装替换掉name字段
                if (fields != null && !fields.isEmpty()) {
                    Text[] fragments = fields.get(ESConstant.NAME_FIELD).getFragments();
                    StringBuilder builder = new StringBuilder();
                    for (Text fragment : fragments) builder.append(fragment.string());
                    courseIndex.setName(builder.toString());
                }
            }
            // 3. 添加到返回集合中
            courseIndexList.add(courseIndex);
        }
        return courseIndexList;
    }

    /**
     * 配置排序
     *
     * @param searchCourseParamDTO 条件参数
     * @param searchRequest        搜索请求
     */
    private void setSort(SearchCourseParamDTO searchCourseParamDTO, SearchRequest searchRequest) {
        if (StrUtil.isNotBlank(searchCourseParamDTO.getSortType()) && searchCourseParamDTO.getSortType().equals(ESConstant.SORT_BY_PRICE_ASC))
            searchRequest.source().sort(ESConstant.PRICE_FIELD, SortOrder.ASC);
        if (StrUtil.isNotBlank(searchCourseParamDTO.getSortType()) && searchCourseParamDTO.getSortType().equals(ESConstant.SORT_BY_PRICE_DESC))
            searchRequest.source().sort(ESConstant.PRICE_FIELD, SortOrder.DESC);
    }

    /**
     * 精确匹配，这些是一定要满足，但不参与算分
     *
     * @param searchCourseParamDTO 条件参数
     * @param boolQueryBuilder     构建器
     */
    private void termFilter(SearchCourseParamDTO searchCourseParamDTO, BoolQueryBuilder boolQueryBuilder) {
        if (StrUtil.isNotBlank(searchCourseParamDTO.getMt()))
            boolQueryBuilder.filter(QueryBuilders.termQuery(ESConstant.MT_NAME_FIELD, searchCourseParamDTO.getMt()));
        if (StrUtil.isNotBlank(searchCourseParamDTO.getSt()))
            boolQueryBuilder.filter(QueryBuilders.termQuery(ESConstant.ST_NAME_FIELD, searchCourseParamDTO.getSt()));
        if (StrUtil.isNotBlank(searchCourseParamDTO.getGrade()))
            boolQueryBuilder.filter(QueryBuilders.termQuery(ESConstant.GRADE_FIELD, searchCourseParamDTO.getGrade()));
        boolQueryBuilder.filter(QueryBuilders.termQuery(ESConstant.STATUS_FIELD, CoursePublishStatusConstant.PUBLISHED));
    }

    /**
     * 模糊匹配和高亮，这些是参与算分的
     *
     * @param searchCourseParamDTO 条件参数
     * @param boolQueryBuilder     构建器
     * @param searchRequest        搜索请求
     */
    private void multiMatchAndHighlight(SearchCourseParamDTO searchCourseParamDTO, BoolQueryBuilder boolQueryBuilder, SearchRequest searchRequest) {
        // 1. 多字段模糊匹配，must参与算分
        boolQueryBuilder.must(
                QueryBuilders.multiMatchQuery(searchCourseParamDTO.getKeywords(), ESConstant.NAME_FIELD, ESConstant.DESCRIPTION_FIELD)
                        .minimumShouldMatch(ESConstant.MINIMUM_MATCH)
                        .field(ESConstant.NAME_FIELD, ESConstant.BOOST_FACTOR)
        );
        // 2. 给名称字段设置高亮
        searchRequest.source().highlighter(new HighlightBuilder().field(ESConstant.NAME_FIELD)
                .preTags(ESConstant.PRE_TAG)
                .postTags(ESConstant.POST_TAG));
    }

    /**
     * 获取聚合结果
     *
     * @param agg 聚合对象
     * @return 聚合结果
     */
    private List<String> getAggList(Terms agg) {
        List<? extends Terms.Bucket> buckets = agg.getBuckets();
        List<String> result = new ArrayList<>(buckets.size());
        for (Terms.Bucket bucket : buckets) {
            String filed = bucket.getKeyAsString();
            result.add(filed);
        }
        return result;
    }
}
